﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.IO;
using System.Diagnostics;
using System.Xml.Linq;
using System.Xml;

namespace AllTalkProxy
{
    public class Listener
    {
        private Stream _stream;
        private Task _receivingTask;
        private CancellationTokenSource _tokenSource;
        private ResponseParser _responseParser;

        public EventHandler<JidEventArgs> JidAvaiable { get; set; }

        public EventHandler<PresenceEventArgs> PresenceChanged { get; set; }

        public EventHandler<MessageEventArgs> MessageReceived { get; set; }

        public EventHandler<RosterLoadedEventArgs> RosterLoaded { get; set; }

        public EventHandler<VCardEventArgs> VcardAvailable { get; set; }

        public Listener(Stream stream)
        {
            _stream = stream;
            _responseParser = new ResponseParser(this);
        }

        public void BeginListen()
        {
            Debug.Assert(_tokenSource == null, "Re-using listener?");
            Debug.Assert(_receivingTask == null, "Re-using listener?");
            _tokenSource = new CancellationTokenSource();
            _receivingTask = Task.Factory.StartNew(() => StartReceive(_tokenSource.Token), _tokenSource.Token);
        }

        public void EndListen()
        {
            _tokenSource.Cancel();
            if (!_receivingTask.Wait(Constants.BindTimeout))
            {
                throw new TimeoutException("Receiving task did not shutdown.");
            }
            if (_receivingTask != null)
            { 
                _receivingTask.Dispose();
                _receivingTask = null;
            }
            if (_stream != null)
            {
                _stream.Close();
            }
        }

        private void StartReceive(CancellationToken token)
        {
            while (!token.IsCancellationRequested)
            {
                try
                {
                    string response = this.Receive();
                    _responseParser.Parse(response);
                }
                catch (IOException ex)
                {
                    Debug.WriteLine("Exception:" + ex.Message);
                }
                catch (UnauthorizedAccessException ex)
                {
                    // Exit from the thread.
                    break;
                }
            }
        }

        internal string Receive(string endingToken = "")
        {
            return Receive(_stream, endingToken);
        }

        internal static string Receive(Stream stream, string endingToken = "")
        {
            byte[] buffer = new byte[4096];
            StringBuilder sb = new StringBuilder();
            int charsRead;
            string currentChunk;
            string startTag = null;
            string endTag = null;
            do
            {
                charsRead = stream.Read(buffer, 0, buffer.Length);
                currentChunk = Encoding.UTF8.GetString(buffer, 0, charsRead);

                int countOfControlChars = 0;
                if (charsRead > 0)
                {
                    countOfControlChars = GetCountOfControlChars(currentChunk);
                }
                sb.Append(currentChunk, countOfControlChars, currentChunk.Length - countOfControlChars);

                if (startTag == null)
                {
                    startTag = ParseFirstTagName(sb);
                }

                // There is no corresponding end tag for this. Add one and exit.
                if (String.Equals(startTag, "stream:stream"))
                {
                    sb.Append("</stream:stream>"); 
                    break;
                }
                
                if (!String.IsNullOrEmpty(endingToken) && currentChunk.EndsWith(endingToken))
                {
                    break;
                }

                if (startTag == null)
                {
                    continue;
                }
                
                endTag = ParseEndTag(sb);
                if (endTag != null &&
                    (StringComparer.Ordinal.Equals(startTag, endTag) || endTag.Length == 0))
                {
                    break;
                }
               
            } while (charsRead > 0);
            string response = sb.ToString();
            Logger.Log(response, "Response:");
            return response;
        }

        private static int GetCountOfControlChars(string currentChunk)
        {
            int countOfControlChars = 0;
            while (true)
            {
                switch (currentChunk[countOfControlChars])
                {
                    case '\u0017':
                    case '\u0003':
                    case '\u0001':
                        countOfControlChars++;
                        break;
                    default:
                        return countOfControlChars;
                }
            }
        }

        private static string ParseEndTag(StringBuilder buffer)
        {
            if (buffer.Length == 0)
            {
                return null;
            }
            string endTag = null;
            int current = buffer.Length - 1;
            int start = -1;
            int end = -1;
            while (current >= 0)
            {
                switch(buffer[current])
                {
                    case '>':
                        end = current;
                        break;
                    case '/':
                        if (current == end - 1)
                        {
                            // empty tag
                            // TODO: Do more validation to see that the element is complete
                            return String.Empty;
                        }
                        else if(end != -1 && current != 0 && buffer[current - 1] == '<')
                        {
                            start = current + 1;
                        }
                        break;
                    // whitespace characters
                    case ' ':
                    case '\t':
                    case '\r':
                    case '\n':
                        break;
                    default:
                        if (end == -1)
                        {
                            return null;
                        }
                        break;
                }
                if (start != -1)
                {
                    break;
                }
                current--;
            }
            if (start != -1)
            {
                char[] endTagArray = new char[end - start];
                buffer.CopyTo(start, endTagArray, 0, end - start);
                endTag = new String(endTagArray);
            }
            return endTag;
        }

        private static string ParseFirstTagName(StringBuilder buffer)
        {
            if (buffer.Length == 0)
            {
                return null;
            }
            string firstTag = null;
            int current = 0;
            int start = -1;
            int end = -1;
            while (current < buffer.Length)
            {
                switch (buffer[current])
                {
                    case '<':
                        start = current + 1;
                        break;
                   // whitespace characters
                    case ' ':
                    case '\t':
                    case '\r':
                    case '\n':
                        if (start >= 0)
                        {
                            end = current;
                        }
                        break;
                }
                if (end != -1)
                {
                    break;
                }
                current++;
            }
            if (end != -1)
            {
                char[] firstTagArray = new char[end - start];
                buffer.CopyTo(start, firstTagArray, 0, end - start);
                firstTag = new String(firstTagArray);
            }
            return firstTag;
        }

        public void RaiseJidAvaible(Jid jid)
        {
            if (JidAvaiable != null)
            {
                JidAvaiable(this, new JidEventArgs(jid));
            }
        }

        public void RaiseJidAvaible(Exception exception)
        {
            if (JidAvaiable != null)
            {
                JidAvaiable(this, new JidEventArgs(exception));
            }
        }

        public void RaisePresenceChanged(string presenceXml)
        {
            if (PresenceChanged != null)
            {
                PresenceChanged(this, new PresenceEventArgs(presenceXml));
            }
        }

        public void RaiseMessageReceived(string messageXml)
        {
            if (MessageReceived != null)
            {
                MessageReceived(this, new MessageEventArgs(messageXml));
            }
        }

        public void RaiseRosterLoaded(string rosterXml)
        {
            if (RosterLoaded != null)
            {
                RosterLoaded(this, new RosterLoadedEventArgs(rosterXml));
            }
        }

        internal void RaiseVcardAvailable(string vcardXml, string userId)
        {
            if (VcardAvailable != null)
            {
                VcardAvailable(this, new VCardEventArgs(vcardXml, userId));
            }
        }
    }

    public class JidEventArgs : EventArgs
    {
        public Jid Jid { get; private set; }
        public Exception Exception { get; private set; }

        public JidEventArgs(Jid jid)
        {
            this.Jid = jid;
        }

        public JidEventArgs(Exception exception)
        {
            this.Exception = exception;
        }
    }
}
